Merge branch 'enhance-data_output_agent'

Akinori MUSHA 9 years ago
parent
commit
f5cf2fd480
2 changed files with 110 additions and 22 deletions
  1. 55 19
      app/models/agents/data_output_agent.rb
  2. 55 3
      spec/models/agents/data_output_agent_spec.rb

+ 55 - 19
app/models/agents/data_output_agent.rb

@@ -8,7 +8,7 @@ module Agents
8 8
 
9 9
         This Agent will output data at:
10 10
 
11
-        `https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || '<id>'}/:secret.xml`
11
+        `https://#{ENV['DOMAIN']}#{Rails.application.routes.url_helpers.web_requests_path(agent_id: ':id', user_id: user_id, secret: ':secret', format: :xml)}`
12 12
 
13 13
         where `:secret` is one of the allowed secrets specified in your options and the extension can be `xml` or `json`.
14 14
 
@@ -19,9 +19,9 @@ module Agents
19 19
 
20 20
           * `secrets` - An array of tokens that the requestor must provide for light-weight authentication.
21 21
           * `expected_receive_period_in_days` - How often you expect data to be received by this Agent from other Agents.
22
-          * `template` - A JSON object representing a mapping between item output keys and incoming event values. Use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the values. The `item` key will be repeated for every Event. The `pubDate` key for each item will have the creation time of the Event unless given.
22
+          * `template` - A JSON object representing a mapping between item output keys and incoming event values.  Use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the values.  Values of the `link`, `title`, `description` and `icon` keys will be put into the \\<channel\\> section of RSS output.  The `item` key will be repeated for every Event.  The `pubDate` key for each item will have the creation time of the Event unless given.
23 23
           * `events_to_show` - The number of events to output in RSS or JSON. (default: `40`)
24
-          * `ttl` - A value for the <ttl> element in RSS output. (default: `60`)
24
+          * `ttl` - A value for the \\<ttl\\> element in RSS output. (default: `60`)
25 25
 
26 26
         If you'd like to output RSS tags with attributes, such as `enclosure`, use something like the following in your `template`:
27 27
 
@@ -39,6 +39,13 @@ module Agents
39 39
               },
40 40
               "_contents": "tag contents (can be an object for nesting)"
41 41
             }
42
+
43
+        # Liquid Templating
44
+
45
+        In Liquid templating, the following variable is available:
46
+
47
+        * `events`: An array of events being output, sorted in descending order up to `events_to_show` in number.  For example, if source events contain a site title in the `site_title` key, you can refer to it in `template.title` by putting `{{events.first.site_title}}`.
48
+
42 49
       MD
43 50
     end
44 51
 
@@ -63,7 +70,17 @@ module Agents
63 70
     end
64 71
 
65 72
     def validate_options
66
-      unless options['secrets'].is_a?(Array) && options['secrets'].length > 0
73
+      if options['secrets'].is_a?(Array) && options['secrets'].length > 0
74
+        options['secrets'].each do |secret|
75
+          case secret
76
+          when %r{[/.]}
77
+            errors.add(:base, "secret may not contain a slash or dot")
78
+          when String
79
+          else
80
+            errors.add(:base, "secret must be a string")
81
+          end
82
+        end
83
+      else
67 84
         errors.add(:base, "Please specify one or more secrets for 'authenticating' incoming feed requests")
68 85
       end
69 86
 
@@ -92,15 +109,39 @@ module Agents
92 109
       interpolated['template']['link'].presence || "https://#{ENV['DOMAIN']}"
93 110
     end
94 111
 
112
+    def feed_url(options = {})
113
+      feed_link + Rails.application.routes.url_helpers.
114
+                  web_requests_path(agent_id: id || ':id',
115
+                                    user_id: user_id,
116
+                                    secret: options[:secret],
117
+                                    format: options[:format])
118
+    end
119
+
120
+    def feed_icon
121
+      interpolated['template']['icon'].presence || feed_link + '/favicon.ico'
122
+    end
123
+
95 124
     def feed_description
96 125
       interpolated['template']['description'].presence || "A feed of Events received by the '#{name}' Huginn Agent"
97 126
     end
98 127
 
99 128
     def receive_web_request(params, method, format)
100
-      if interpolated['secrets'].include?(params['secret'])
101
-        items = received_events.order('id desc').limit(events_to_show).map do |event|
129
+      unless interpolated['secrets'].include?(params['secret'])
130
+        if format =~ /json/
131
+          return [{ error: "Not Authorized" }, 401]
132
+        else
133
+          return ["Not Authorized", 401]
134
+        end
135
+      end
136
+
137
+      source_events = received_events.order(id: :desc).limit(events_to_show).to_a
138
+
139
+      interpolation_context.stack do
140
+        interpolation_context['events'] = source_events
141
+
142
+        items = source_events.map do |event|
102 143
           interpolated = interpolate_options(options['template']['item'], event)
103
-          interpolated['guid'] = {'_attributes' => {'isPermaLink' => 'false'}, 
144
+          interpolated['guid'] = {'_attributes' => {'isPermaLink' => 'false'},
104 145
                                   '_contents' => interpolated['guid'].presence || event.id}
105 146
           date_string = interpolated['pubDate'].to_s
106 147
           date =
@@ -128,12 +169,13 @@ module Agents
128 169
             <?xml version="1.0" encoding="UTF-8" ?>
129 170
             <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
130 171
             <channel>
131
-             <atom:link href="#{feed_link.encode(:xml => :text)}/users/#{user.id}/web_requests/#{id || '<id>'}/#{params['secret']}.xml" rel="self" type="application/rss+xml" />
132
-             <title>#{feed_title.encode(:xml => :text)}</title>
133
-             <description>#{feed_description.encode(:xml => :text)}</description>
134
-             <link>#{feed_link.encode(:xml => :text)}</link>
135
-             <lastBuildDate>#{Time.now.rfc2822.to_s.encode(:xml => :text)}</lastBuildDate>
136
-             <pubDate>#{Time.now.rfc2822.to_s.encode(:xml => :text)}</pubDate>
172
+             <atom:link href=#{feed_url(secret: params['secret'], format: :xml).encode(xml: :attr)} rel="self" type="application/rss+xml" />
173
+             <atom:icon>#{feed_icon.encode(xml: :text)}</atom:icon>
174
+             <title>#{feed_title.encode(xml: :text)}</title>
175
+             <description>#{feed_description.encode(xml: :text)}</description>
176
+             <link>#{feed_link.encode(xml: :text)}</link>
177
+             <lastBuildDate>#{Time.now.rfc2822.to_s.encode(xml: :text)}</lastBuildDate>
178
+             <pubDate>#{Time.now.rfc2822.to_s.encode(xml: :text)}</pubDate>
137 179
              <ttl>#{feed_ttl}</ttl>
138 180
 
139 181
           XML
@@ -147,12 +189,6 @@ module Agents
147 189
 
148 190
           return [content, 200, 'text/xml']
149 191
         end
150
-      else
151
-        if format =~ /json/
152
-          return [{ :error => "Not Authorized" }, 401]
153
-        else
154
-          return ["Not Authorized", 401]
155
-        end
156 192
       end
157 193
     end
158 194
 

+ 55 - 3
spec/models/agents/data_output_agent_spec.rb

@@ -34,8 +34,18 @@ describe Agents::DataOutputAgent do
34 34
       expect(agent).not_to be_valid
35 35
       agent.options[:secrets] = "foo"
36 36
       expect(agent).not_to be_valid
37
+      agent.options[:secrets] = "foo/bar"
38
+      expect(agent).not_to be_valid
39
+      agent.options[:secrets] = "foo.xml"
40
+      expect(agent).not_to be_valid
41
+      agent.options[:secrets] = false
42
+      expect(agent).not_to be_valid
37 43
       agent.options[:secrets] = []
38 44
       expect(agent).not_to be_valid
45
+      agent.options[:secrets] = ["foo.xml"]
46
+      expect(agent).not_to be_valid
47
+      agent.options[:secrets] = ["hello", true]
48
+      expect(agent).not_to be_valid
39 49
       agent.options[:secrets] = ["hello"]
40 50
       expect(agent).to be_valid
41 51
       agent.options[:secrets] = ["hello", "world"]
@@ -83,9 +93,10 @@ describe Agents::DataOutputAgent do
83 93
       expect(status).to eq(200)
84 94
     end
85 95
 
86
-    describe "outputtng events as RSS and JSON" do
96
+    describe "outputting events as RSS and JSON" do
87 97
       let!(:event1) do
88 98
         agents(:bob_website_agent).create_event :payload => {
99
+          "site_title" => "XKCD",
89 100
           "url" => "http://imgs.xkcd.com/comics/evolving.png",
90 101
           "title" => "Evolving",
91 102
           "hovertext" => "Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution."
@@ -94,6 +105,7 @@ describe Agents::DataOutputAgent do
94 105
 
95 106
       let!(:event2) do
96 107
         agents(:bob_website_agent).create_event :payload => {
108
+          "site_title" => "XKCD",
97 109
           "url" => "http://imgs.xkcd.com/comics/evolving2.png",
98 110
           "title" => "Evolving again",
99 111
           "date" => '',
@@ -103,6 +115,7 @@ describe Agents::DataOutputAgent do
103 115
 
104 116
       let!(:event3) do
105 117
         agents(:bob_website_agent).create_event :payload => {
118
+          "site_title" => "XKCD",
106 119
           "url" => "http://imgs.xkcd.com/comics/evolving0.png",
107 120
           "title" => "Evolving yet again with a past date",
108 121
           "date" => '2014/05/05',
@@ -119,7 +132,8 @@ describe Agents::DataOutputAgent do
119 132
           <?xml version="1.0" encoding="UTF-8" ?>
120 133
           <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
121 134
           <channel>
122
-           <atom:linkhref="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
135
+           <atom:link href="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
136
+           <atom:icon>https://yoursite.com/favicon.ico</atom:icon>
123 137
            <title>XKCD comics as a feed</title>
124 138
            <description>This is a feed of recent XKCD comics, generated by Huginn</description>
125 139
            <link>https://yoursite.com</link>
@@ -194,6 +208,43 @@ describe Agents::DataOutputAgent do
194 208
           ]
195 209
         })
196 210
       end
211
+
212
+      describe "interpolating \"events\"" do
213
+        before do
214
+          agent.options['template']['title'] = "XKCD comics as a feed{% if events.first.site_title %} ({{events.first.site_title}}){% endif %}"
215
+          agent.save!
216
+        end
217
+
218
+        it "can output RSS" do
219
+          stub(agent).feed_link { "https://yoursite.com" }
220
+          content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
221
+          expect(status).to eq(200)
222
+          expect(content_type).to eq('text/xml')
223
+          expect(Nokogiri(content).at('/rss/channel/title/text()').text).to eq('XKCD comics as a feed (XKCD)')
224
+        end
225
+
226
+        it "can output JSON" do
227
+          content, status, content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
228
+          expect(status).to eq(200)
229
+
230
+          expect(content['title']).to eq('XKCD comics as a feed (XKCD)')
231
+        end
232
+      end
233
+
234
+      describe "with a specified icon" do
235
+        before do
236
+          agent.options['template']['icon'] = 'https://somesite.com/icon.png'
237
+          agent.save!
238
+        end
239
+
240
+        it "can output RSS" do
241
+          stub(agent).feed_link { "https://yoursite.com" }
242
+          content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
243
+          expect(status).to eq(200)
244
+          expect(content_type).to eq('text/xml')
245
+          expect(Nokogiri(content).at('/rss/channel/atom:icon/text()').text).to eq('https://somesite.com/icon.png')
246
+        end
247
+      end
197 248
     end
198 249
 
199 250
     describe "outputting nesting" do
@@ -294,7 +345,8 @@ describe Agents::DataOutputAgent do
294 345
           <?xml version="1.0" encoding="UTF-8" ?>
295 346
           <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
296 347
           <channel>
297
-           <atom:linkhref="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
348
+           <atom:link href="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
349
+           <atom:icon>https://yoursite.com/favicon.ico</atom:icon>
298 350
            <title>XKCD comics as a feed</title>
299 351
            <description>This is a feed of recent XKCD comics, generated by Huginn</description>
300 352
            <link>https://yoursite.com</link>